package app.caihan.scframe.base.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.caihan.scframe.api.retrofit2.*
import app.caihan.scframe.constant.API_TAG
import app.caihan.scframe.util.CoroutineState
import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

/**
 * ViewModel的目标
 * 1.让状态管理独立于视图控制器，从而做到重建状态的分治、状态在多页面的共享，以及跨页面通信。
 * 2.为状态设置作用域，使状态的共享做到作用域可控。
 * 3.实现单向依赖，避免内存泄漏。
 * ViewModel无需实现DefaultLifecycleObserver接口绑定页面的生命周期,
 * Activity和Fragment，是作为ViewModelStoreOwner的实现，每个ViewModelStoreOwner持有着唯一的一个ViewModelStore，也即每个视图控制器都持有自己的一个ViewModelStore
 * ViewModelStore 的作用是维护一个 Map，来管理视图控制器所持有的所有 ViewModel(ViewModelStoreOwner 持有 ViewModelStore 持有 ViewModel)
 * 当作为 ViewModelStoreOwner 的视图控制器被 destory 时（重建的情况除外），ViewModelStore 会被通知去完成清除状态操作，从而将 Map 中管理着的 ViewModel 全部走一遍 clear 方法，并且清空 Map
 * See the [androidx.lifecycle.ViewModelProvider.get]
 *
 * 不建议在ViewModel中写业务逻辑的理由是
 * ViewModel的生命周期是跟随ViewModelStoreOwner的，Owner一旦destoryed，那么ViewModel也会走 clear，而触发 onClear。
 * 如果你在onClear中手动置空了业务变量，而业务方法此时又在执行中，那么就容易造成空指针异常。
 * 业务逻辑应该放在具体的数据组件中写，例如DataRepository，作为基础数据组件，既持有了httpManager，又持有了dbManager，各种缓存策略，以最合适的方式拿到前台 UI 想要的状态数据。
 * 在数据组件中处理业务逻辑，通过唯一可信源，以数据驱动的方式来统一将状态分发给前台UI，于是当前台UI健在，就会接受数据、响应 UI 逻辑，否则就会因LifeCycler的加持，而不至于让数据接触到空的视图引用、造成不可预期的错误
 *
 * 当 viewModelScope 被清除 (当 onCleared() 回调被调用时) 之后，它将自动取消它所启动的所有协程
 *
 * @author Seven-C
 * @date 2020/7/27
 */
open class BaseViewModel : ViewModel() {

    //协程状态管理
    // 私有的 MutableLiveData 可变的，对内访问
    private val _statusLiveData: MutableLiveData<CoroutineState> by lazy {
        MutableLiveData<CoroutineState>()
    }

    //异常数据管理
    private val _errorLiveData: MutableLiveData<ResponseThrowable> by lazy {
        MutableLiveData<ResponseThrowable>()
    }

    // 对外暴露不可变的 LiveData，只能查询
    val statusLiveData: LiveData<CoroutineState> = _statusLiveData
    val errorLiveData: LiveData<ResponseThrowable> = _errorLiveData


    protected fun showLoadingView() {
        if (_statusLiveData.value != CoroutineState.START) {
            _statusLiveData.value = CoroutineState.START
        }
    }

    protected fun showContentView() {
        if (_statusLiveData.value != CoroutineState.FINISH) {
            _statusLiveData.value = CoroutineState.FINISH
        }
    }

    private fun showErrorView() {
        if (_statusLiveData.value != CoroutineState.ERROR) {
            _statusLiveData.value = CoroutineState.ERROR
        }
    }


    /**
     * 所有网络请求都在 viewModelScope 域中启动，当页面销毁时会自动
     * 调用ViewModel的  #onCleared 方法取消所有协程
     * viewModelScope 默认使用 Dispatchers.Main, 方便 Activity 和 Fragment 更新 UI
     * Launch 是将代码从常规函数送往协程世界的桥梁
     * 在 viewModel 中使用 viewModelScope.launch 启动协程,可以避免内存泄漏,因为 viewModelScope 会自动帮你取消协程
     * viewModelScope.launch 默认是使用Dispatchers.Main线程,方便更新UI,所以网络请求请自行切换到IO线程
     */
    fun launchUI(block: suspend CoroutineScope.() -> Unit) = viewModelScope.launch {
        try {
            block()
        } catch (e: Exception) {
            ExceptionHandle.handleException(e).printStackTrace()
        }
    }

    /**
     * net request 只返回成功数据
     * 异常数据会保存到errorLiveData中
     * viewModelScope.launch 默认是使用Dispatchers.Main线程,方便更新UI,所以网络请求请自行切换到IO线程
     *
     * @param block 请求体方法，必须要用suspend关键字修饰
     * @param result 请求成功结果赋值
     * @param isShowLoadingView 是否显示页面Loading布局,默认false=不显示
     * @param isShowErrorView 是否显示页面Error布局,默认false=不显示
     * @param isShowErrorToast 是否显示页面Error布局,默认true=显示
     */
    fun <T> requestOnlySuccess(
        block: suspend () -> ScResult<T>,
        result: MutableLiveData<T>,
        isShowLoadingView: Boolean = false,
        isShowErrorView: Boolean = false,
        isShowErrorToast: Boolean = true
    ) = request(
        block,
        { result.value = it },
        {},
        { isShowLoadingView },
        { isShowErrorView },
        { isShowErrorToast })


    /**
     * net request 返回成功请求与异常统一处理
     * viewModelScope.launch 默认是使用Dispatchers.Main线程,方便更新UI,所以网络请求请自行切换到IO线程
     *
     * @param block 请求体方法，必须要用suspend关键字修饰
     * @param success 请求成功回调
     * @param error 请求失败回调,可不传
     * @param isShowLoadingView 是否显示页面Loading布局,默认true=显示
     * @param isShowErrorView 是否显示页面Error布局,默认true=显示
     * @param isShowErrorToast 是否显示错误日志,如果isShowErrorView=true的话,错误日志会在errorView上显示,反之是弹toast
     */
    private fun <T> request(
        block: suspend () -> ScResult<T>,
        success: (T) -> Unit,
        error: (ResponseThrowable) -> Unit = {},
        isShowLoadingView: () -> Boolean = { true },
        isShowErrorView: () -> Boolean = { true },
        isShowErrorToast: () -> Boolean = { true }
    ) = viewModelScope.launch {
        runCatching {
            if (isShowLoadingView()) showLoadingView()
            //请求体
            block()
        }.onSuccess {
            if (it is ScResult.Success) {
                showContentView()
                success(it.data)
            } else if (it is ScResult.Error) {
                if (isShowErrorView()) showErrorView()
                if (isShowErrorToast()) _errorLiveData.postValue(it.throwable)
                error(it.throwable)
                LogUtils.dTag(API_TAG, "request ScResult.Error :: $it")
            }
        }.onFailure {
            val errorResult = ScResult.Error(ExceptionHandle.handleException(it))
            if (isShowErrorView()) showErrorView()
            if (isShowErrorToast()) _errorLiveData.postValue(errorResult.throwable)
            error(errorResult.throwable)
            LogUtils.dTag(API_TAG, "request onFailure :: $it")
        }
    }


    /**
     * 用流的方式进行网络请求
     *
     * 什么时候使用 Flow ？
     * Flow 是一种 "冷流"(Cold Stream)。"冷流" 是一种数据源，
     * 该类数据源的生产者会在每个监听者开始消费事件的时候执行，从而在每个订阅上创建新的数据流。
     * 一旦消费者停止监听或者生产者的阻塞结束，数据流将会被自动关闭。
     *
     * Flow 非常适合需要开始/停止数据的产生来匹配观察者的场景。
     */
    fun <T> launchFlow(block: suspend () -> T): Flow<T> {
        return flow {
            // 生产者代码开始执行，流被打开
            emit(block())
            // 生产者代码结束，流将被关闭
        }
    }

    @ExperimentalCoroutinesApi
    inline fun <reified T> Flow<ScResult<T>>.applyTransform(): Flow<Any?> {
        return this
            .flowOn(Dispatchers.IO)
            .map { result ->
                result.doSuccess {
                    return@map it
                }
                result.doFailure {
                    return@map it
                }
            }
    }
}